כולנו מכירים את הסיטואציה הזו: בעבודה יש לנו את הכלים הכי מתקדמים, נוח לנו לעבוד והכל מעולה. אבל כשזה מגיע לפרוייקטים אישיים יש משהו אחר שמפריע; אם זה המחשב שלא עובד, סביבת העבודה או התוכנות במחשב. היום, אדבר על איך נוכל לקחת Schema ולממש Dataset פיקטיבי, להריץ SQL ב-Google BigQuery ולענות על שאלות. השאלות עליהן נענה נלקחו מתוך Arena Games - Data Analysis שבנה רם קדם ופורסמו לתרגול, וכל הקרדיט לשאלות שייך ל-UpScale Analytics.
מבנה מסד הנתונים כולל נתונים על משחקים ועונות של שחקנים:
לפני שנתחיל, אמליץ על “SQL Fundamentals” באתר Datacamp. אוסף קורסים שמלמדים לכדי ביצוע שאילתות מורכבות, עם סרטונים קצרים ותרגול לכול אורכם. השתמשתי באתר Workflowy על מנת לסכם את הקורסים.
Google BigQuery הינו בין הכלים המתקדמים בשוק לביצוע שאילתות על גבי מסדי נתונים גדולים, בצורה מהירה ונוחה. בואו נגדיר Dataset ונתחיל לבנות אותו בשלבים:
ניכנס לאתר של Google Cloud, נירשם עם חשבון ה-Google שלנו.
נעבור למוצר BigQuery ונגדיר Dataset חדש וריק. אני קראתי לו ״Arena״.
בתוך הלינק שצירפתי מעלה, מצורף ה-Schema של מסד הנתונים. היא מותאמת למסד נתונים מסוג MSSQL, ו-BigQuery תומכים ב-״BigQuery Standard SQL״, שזו גרסה מעט שונה עם שינויים ושיפורים. לכן, נצטרך להמיר את המבנה לפורמט ש-BigQuery יכירו.
העתקתי את ה-Schema שכתובה שם ל-ChatGPT (למודל GPT-4), וביקשתי ממנו לבצע את ההמרה הנדרשת לפורמט ש-BigQuery יכיר. צירפתי מטה את התוצאה, שימו לב שלפני Arena מצויין project ID בו נמצא Dataset אותו אנחנו בונים.
-- Structure for game_sessions table
CREATETABLE`teak-ellipse-401213.Arena.game_sessions`(session_idINT64NOTNULL,session_begin_dateTIMESTAMP,session_end_dateTIMESTAMP,player_idINT64,game_idINT64);-- Structure for games table
CREATETABLE`teak-ellipse-401213.Arena.games`(idINT64NOTNULL,game_nameSTRING);-- Structure for paying_method table
CREATETABLE`teak-ellipse-401213.Arena.paying_method`(player_idINT64NOTNULL,credit_card_typeSTRINGNOTNULL,credit_card_numberSTRINGNOTNULL);-- Structure for players table
CREATETABLE`teak-ellipse-401213.Arena.players`(player_idINT64NOTNULL,first_nameSTRING,last_nameSTRING,email_addressSTRING,genderSTRING,age_groupSTRING,countrySTRING,citySTRING,street_addressSTRING);-- Structure for session_details table
CREATETABLE`teak-ellipse-401213.Arena.session_details`(session_idINT64NOTNULL,action_idINT64NOTNULL,action_typeSTRINGNOTNULL,amountNUMERIC);
אחרי שבנינו את תבנית הטבלאות, מה שנשאר לנו זה ליצר Data Synthetic בעזרת ChatGPT. מתפיסתי חשוב שניצמד ככל הניתן לפורמט של מסד הנתונים במקור, ולכן צירפתי ל-Prompt דוגמאות, גם מהקובץ של מסד הנתונים. לאחר מכן בניתי פונקציות Python שעזרו ליצר שאילתות להכנסת המידע לטבלאות שלנו בעזרת ספריית Faker:
generate_game_sessions - פונקציה שמיצרת לנו את ה-Sessions. יש לנו תאריך התחלה וסיום, שחקן ומשחק.
generate_games - פונקציה שמיצרת לנו את ה-Games. יש לנו את ה-ID שלו ושמו של המשחק.
generate_paying_method - פונקציה שמיצרת לנו את שיטת התשלום עבור המשחק. Faker` אפילו תומכת ביצירת מספר אשראי פיקטיבי.
generate_players - פונקציה שמיצרת את השחקנים והפרטים המזהים שלהם.
generate_session_details - פונקציה שמיצרת את הפרטים עבור כל Session ואת נתוני רווח-הפסד שלה.
fromfakerimportFakerimportrandomfake=Faker()defgenerate_game_sessions(num_rows=100):query="INSERT INTO `teak-ellipse-401213.Arena.game_sessions` (session_id, session_begin_date, session_end_date, player_id, game_id) VALUES "values=[]foriinrange(1,num_rows+1):session_id=ibegin_date=fake.date_time_this_decade()end_date=fake.date_time_between_dates(begin_date)player_id=random.randint(1,100)game_id=random.randint(1,4)values.append(f"({session_id}, TIMESTAMP('{begin_date}'), TIMESTAMP('{end_date}'), {player_id}, {game_id})")query+=", ".join(values)returnquerydefgenerate_games(num_rows=100):query="INSERT INTO `teak-ellipse-401213.Arena.games` (id, game_name) VALUES "values=[]foriinrange(1,num_rows+1):game_name=fake.word()values.append(f"({i}, '{game_name}')")query+=", ".join(values)returnquerydefgenerate_paying_method(num_rows=100):query="INSERT INTO teak-ellipse-401213.Arena.paying_method (player_id, credit_card_type, credit_card_number) VALUES "values=[]card_types=['visa','mastercard','amex']foriinrange(1,num_rows+1):player_id=random.randint(1,100)card_type=random.choice(card_types)card_number=fake.credit_card_number(card_type=card_type)values.append(f"({player_id}, '{card_type}', '{card_number}')")query+=", ".join(values)returnquerydefgenerate_players(num_rows=100):query="INSERT INTO `teak-ellipse-401213.Arena.players` (player_id, first_name, last_name, email_address, gender, age_group, country, city, street_address) VALUES "values=[]foriinrange(1,num_rows+1):first_name=fake.first_name()last_name=fake.last_name()email=fake.email()gender=random.choice(['Male','Female'])age_group=random.choice(['10-21','21-30','31-40','41-50','51-60'])country=fake.country()city=fake.city()address=fake.street_address()values.append(f"({i}, '{first_name}', '{last_name}', '{email}', '{gender}', '{age_group}', '{country}', '{city}', '{address}')")query+=", ".join(values)returnquerydefgenerate_session_details(num_rows=100):query="INSERT INTO `teak-ellipse-401213.Arena.session_details` (session_id, action_id, action_type, amount) VALUES "values=[]action_types=['gain','loss']foriinrange(1,num_rows+1):session_id=random.randint(1,100)action_id=iaction_type=random.choice(action_types)amount=round(random.uniform(100,1000),2)values.append(f"({session_id}, {action_id}, '{action_type}', {amount})")query+=", ".join(values)returnquery# Function to get all the insert queriesdefget_insert_queries(num_rows=100):return{'game_sessions':generate_game_sessions(num_rows),'games':generate_games(num_rows),'paying_method':generate_paying_method(num_rows),'players':generate_players(num_rows),'session_details':generate_session_details(num_rows)}# Get all insert queriesinsert_queries=get_insert_queries(100)# File path where you want to save the queriesfile_path="insert_queries.sql"# Open the file in write modewithopen(file_path,'w')asfile:# Iterate over each table and its corresponding queryfortable,queryininsert_queries.items():# Write the comment and the query to the filefile.write(f'-- Insert data into {table}\n')file.write(query)file.write('\n\n')
אחרי שנריץ את הקוד הזה, נקבל 5 שאילתות שיישמרו תחת קובץ בשם ״insert_queries.sql״. את השאילתות נריץ ב-BigQuery ונהיה מוכנים להתחיל לענות על השאלות.
-- Insert data into game_sessions
INSERTINTO`teak-ellipse-401213.Arena.game_sessions`(session_id,session_begin_date,session_end_date,player_id,game_id)VALUES(1,TIMESTAMP('2022-04-18 07:48:12')-- Insert data into games
INSERTINTO`teak-ellipse-401213.Arena.games`(id,game_name)VALUES(1,'letter')-- Insert data into players
INSERTINTO`teak-ellipse-401213.Arena.players`(player_id,first_name,last_name,email_address,gender,age_group,country,city,street_address)VALUES(1,'Misty','Harper','melissawarner@example.net','Female','41-50','Western Sahara','Howardstad','53600 Craig Key')-- Insert data into paying_method
INSERTINTO`teak-ellipse-401213.Arena.paying_method`(player_id,credit_card_type,credit_card_number)VALUES(2,'visa','4812645610428841')-- Insert data into session_details
INSERTINTO`teak-ellipse-401213.Arena.session_details`(session_id,action_id,action_type,amount)VALUES(36,1,'gain',890.87)
אחרי שהכנסנו והגדרנו את מסד הנתונים, נוכל להתחיל לתשאל שאילתות.
בפתרון השאלות אכתוב הערות מרחיבות על דרך הפתרון שלי כשארגיש צורך. במידה ותרצו לראות תפיסה נוספת לפתרונן והסבר מעמיק יותר, אמליץ לכם לצפות ב-Livestream של חברי הטוב Amit Grinson שפתר את אותן השאלות לפני מספר חודשים.
Create a report that displays for each player a single payment method, according to the following preference: American Express, Mastercard, Visa.
That is, if the player has an American Express payment method, we will display it. Otherwise, we will display Mastercard, and if none of the above, we will display Visa.
בפתרון שאלה זו השתמשתי ב-()ROW_NUMBER שעזר לי למספר עבור כל PARTITION של השחקנים את סדר כרטיסי האשראי. באמצעות המיון, יכלתי בשאילתה עצמה לסנן על המיקום הראשון של המיון payment_index, ובכך לקבל את אמצעי התשלום של השחקן על פי הסדר שהתבקש.
Display the number of sessions for each game. Rank the output according to the number of game sessions, from highest to lowest.
-- Question Number 3: Games per Session
WITHsession_rankAS(SELECTgame_name,COUNT(game_id)ASnum_sessionsFROM`teak-ellipse-401213.Arena.game_sessions`asgame_sessionsJOIN`teak-ellipse-401213.Arena.games`asgamesONgame_sessions.game_id=games.IDGROUPBYgame_name)SELECTgame_name,num_sessions,RANK()OVER(ORDERBYnum_sessionsDESC)ASrow_nFROMsession_rankORDERBYnum_sessionsDESC
Rank the games according to the total amount of minutes played in each one.
-- Question Number 4: Total Game Duration
SELECTgame_name,duration,RANK()OVER(ORDERBYdurationDESC)ASrow_nFROM(SELECTgame_id,SUM(TIMESTAMP_DIFF(session_end_date,session_begin_date,MINUTE))ASdurationFROM`teak-ellipse-401213.Arena.game_sessions`asgame_sessionsGROUPBYgame_id)ASgames_durationsJOIN`teak-ellipse-401213.Arena.games`asgamesONgames_durations.game_id=games.IDORDERBYdurationDESC
For each age-group display the game in which most time was spent
-- Question Number 5: Duration per Age Group
WITHgames_durationsAS(SELECTage_group,game_name,SUM(TIMESTAMP_DIFF(session_end_date,session_begin_date,MINUTE))ASdurationFROM`teak-ellipse-401213.Arena.game_sessions`asgame_sessionsJOIN`teak-ellipse-401213.Arena.players`asplayersONgame_sessions.player_id=players.player_idJOIN`teak-ellipse-401213.Arena.games`asgamesONgame_sessions.game_id=games.IDGROUPBYage_group,game_name)SELECTage_group,game_name,durationFROM(SELECTage_group,game_name,duration,ROW_NUMBER()OVER(PARTITIONBYgame_nameORDERBYdurationDESC)ASrankFROMgames_durations)WHERErank=1;
-- Question Number 6: Balance per Game
SELECTsession_id,action_id,action_type,amount,SUM(CASEWHENaction_type="gain"THENamountELSE-amountEND)OVER(ORDERBYsession_id,action_id)AStotal_amountFROM`teak-ellipse-401213.Arena.session_details`ASsession_detailsORDERBYsession_id,action_id
How many game sessions ended with a profit, how many game sessions ended with a loss, and how many ended in a draw?
האמת שאני מעדיף להימנע בכתיבה בגוף השאילתה ערכים של עמודות. ייתכן שיש פה מקרה קלאסי לצורך של חיבור Python על מנת לוודא את ערכי עמודת `action_type`. לשם העניין שלנו, עשיתי בדיקה בשאילתה נפרדת.
-- Question Number 7: Action Type Stats
-- Check distinct values of 'action_type'
SELECTDISTINCTaction_typeFROM`teak-ellipse-401213.Arena.session_details`ASsession_details;SELECTCOUNTIF(action_type='gain')ASprofit_total,COUNTIF(action_type='loss')ASloss_totalFROM`teak-ellipse-401213.Arena.session_details`ASsession_details;
How many game sessions ended with a profit, how many game sessions ended with a loss, and how many ended in a draw. Break down the result for each gender and age group.
-- Question Number 8: Game Sessions Stats
WITHgame_statsAS(SELECTgender,age_group,action_type,game_idFROM`teak-ellipse-401213.Arena.session_details`ASsession_detailsJOIN`teak-ellipse-401213.Arena.game_sessions`ASgame_sessionsONsession_details.session_id=game_sessions.session_idJOIN`teak-ellipse-401213.Arena.players`ASplayersONgame_sessions.player_id=players.player_id)SELECT*FROMgame_statsPIVOT(COUNT(DISTINCTgame_id)FORaction_typeIN('gain','loss'))ORDERBYgender,age_group
What is the total profit/loss amount for each player?
-- Question Number 9: Total Profit/Loss for each player
SELECTplayer_id,SUM(profit)AStotal_gain_lossFROM(SELECTplayers.player_id,IF(action_type='gain',amount,-amount)ASprofit,FROM`teak-ellipse-401213.Arena.session_details`ASsession_detailsJOIN`teak-ellipse-401213.Arena.game_sessions`ASgame_sessionsONsession_details.session_id=game_sessions.session_idJOIN`teak-ellipse-401213.Arena.players`ASplayersONgame_sessions.player_id=players.player_id)GROUPBYplayer_id
When a player wins, the house loses, and when a player loses, the house wins. Based on the available information, is the company currently in profit or loss?
בהתחלה לא הבנתי איך אעשה את השאלה הזו, והתחלתי לבנות תתי-שאילתות. אחרי כמה זמן הבנתי שמדובר על מקרה קלאסי של פעולות מסכמות עם `CASE` בתוכן.
-- Question Number 10: House Profit
SELECTSUM(CASEWHENaction_type='loss'THENamountELSE0END)AShouse_gains,-SUM(CASEWHENaction_type='gain'THENamountELSE0END)AShouse_losses,SUM(CASEWHENaction_type='loss'THENamountELSE0END)-SUM(CASEWHENaction_type='gain'THENamountELSE0END)ASoverall_gain_lossFROM`teak-ellipse-401213.Arena.session_details`;
Present the company's profits/losses by year and quarter.
בשאילתה הזו לראשונה שמתי לב ל-Feature ייחודי ב-BigQuery, אפשרות לא לחזור בתוך ה-`GROUP BY` על הפעולות המסכמות מתוך ה-`SELECT`. מדובר על מאפיין חכם שלא הכרתי שקיים, והופך את הקוד לקריא יותר.
-- Question Number 11: House Earnings by Quarters
SELECTEXTRACT(YEARFROMsession_begin_date)asyear,EXTRACT(QUARTERFROMsession_begin_date)asquarter,SUM(CASEWHENaction_type='loss'THENamountELSE0END)AShouse_gains,-SUM(CASEWHENaction_type='gain'THENamountELSE0END)AShouse_losses,SUM(CASEWHENaction_type='loss'THENamountELSE0END)-SUM(CASEWHENaction_type='gain'THENamountELSE0END)ASoverall_gain_lossFROM`teak-ellipse-401213.Arena.session_details`ASsession_detailsJOIN`teak-ellipse-401213.Arena.game_sessions`ASgame_sessionsONsession_details.session_id=game_sessions.session_idGROUPBYyear,quarter-- using aliases in GROUP BY, unique to BigQuery
ORDERBYyear,quarter
אני ממליץ מאוד להשתמש בכלי BigQuery, לפרוייקטים אישיים וללמידה. אישית למדתי מהפרוייקט הזה לא מעט, ואני מקווה שהצלחתי להעביר לכם את הייחודיות ויכולות כלי זה.